﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Data.Common;
using System.Threading;
using System.Collections;
using System.Globalization;

namespace ResourceBlender_Express {
    /// <summary>
    /// Performs translation and shows a log.
    /// </summary>
    public partial class WinTranslator : Window {
        /// <summary>
        /// Gets whether the translation was completed successfully.
        /// </summary>
        /// <value><c>true</c> if successful; otherwise, <c>false</c>.</value>
        public bool Success { get { return success; } }

        private Style styError,
                      stySuccess, // every successful query/translation
                      styStatus, // intermediate status messages
                      styOther;
        private BackgroundWorker worker;
        private static Dictionary<int, PendingTranslation> resources = new Dictionary<int, PendingTranslation>();
        private static ManualResetEvent[] translationsDone = null;

        private static string sourceLanguage, 
                              sourceType,
                              destinationLanguage,
                              destinationType;
        private static bool overwriteExisting,
                            verbose;
        private bool success = false;
        private List<RBApplication> applications;
        private ObservableSortedDictionary<string, string> bundles;

        /// <summary>
        /// Initializes a new <see cref="WinTranslator"/>.
        /// </summary>
        public WinTranslator(string sourceLanguage, string sourceType, string destinationLanguage, string destinationType, List<RBApplication> applications, ObservableSortedDictionary<string, string> bundles, bool overwriteExisting, bool verbose) {
            InitializeComponent();
            WinTranslator.sourceLanguage = sourceLanguage;
            WinTranslator.sourceType = sourceType;
            WinTranslator.destinationLanguage = destinationLanguage;
            WinTranslator.destinationType = destinationType;
            this.applications = applications;
            this.bundles = bundles;
            WinTranslator.overwriteExisting = overwriteExisting;
            WinTranslator.verbose = verbose;
            styError = this.FindResource("ErrorStyle") as Style;
            stySuccess = this.FindResource("SuccessStyle") as Style;
            styStatus = this.FindResource("StatusStyle") as Style;
            styOther = this.FindResource("OtherStyle") as Style;
        }

        /// <summary>
        /// Begins translation when the window has finished loading.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        private void Window_Loaded(object sender, RoutedEventArgs e) {
            worker = new BackgroundWorker();
            worker.WorkerReportsProgress = true;
            worker.WorkerSupportsCancellation = true;
            worker.ProgressChanged += (s, pce) => {
                if(pce.UserState is StatusMessage) {
                    StatusMessage message = pce.UserState as StatusMessage;
                    LogText(message.Message, message.MessageType);
                }
            };
            worker.DoWork += (s, dwe) => {
                StatusMessage.StatusChangedEventHandler logHandler = new StatusMessage.StatusChangedEventHandler(message => {
                    worker.ReportProgress(0, message);
                });
                Database db = new Database();
                int minWorkerThreads,
                        minIOThreads,
                        maxWorkerThreads,
                        maxIOThreads;
                    ThreadPool.GetMinThreads(out minWorkerThreads, out minIOThreads);
                    ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxIOThreads);
                try {
                    db.OpenConnection();
                    ThreadPool.SetMinThreads(Math.Min(2, minWorkerThreads), minIOThreads);
                    ThreadPool.SetMaxThreads(Math.Max(maxWorkerThreads, 10), maxIOThreads);

                    using(Database.Command cmd = db.CreateCommand()) {
                        string[] applicationIds = new string[applications.Count];
                        int i;
                        for(i = 0; i < applications.Count; i++) {
                            applicationIds[i] = applications[i].ID;
                        }
                        // first select COUNT(*) and don't group, then select two fields and group
                        string selectQuery = string.Format(@"SELECT {{0}} FROM {0}elements AS e INNER JOIN {0}resources AS r ON r.resourceid = e.resourceid 
                                                             WHERE{1} AND language = @srcLang{2}{3} {{1}}", db.TablePrefix,
                                                            db.GenerateInClause("applicationid", applicationIds, false),
                                                            db.GenerateInClause("bundle", bundles.Keys.ToArray(), true),
                                                            overwriteExisting ? "" : string.Format(" AND r.resourceid NOT IN (SELECT resourceid FROM {0}resources WHERE language = @destLang)", db.TablePrefix));
                        cmd.AddParameterWithValue("@srcLang", sourceLanguage);
                        cmd.AddParameterWithValue("@destLang", destinationLanguage);

                        cmd.CommandText = string.Format(selectQuery, "COUNT(*)", "");
                        int numResources = int.Parse(cmd.ExecuteScalar().ToString());
                        logHandler(new StatusMessage(string.Format("Found {0} resources to translate", numResources), StatusMessage.StatusMessageType.Status));

                        cmd.CommandText = string.Format(selectQuery, "r.resourceid, r.translation", "GROUP BY r.resourceid, r.translation");
                        DbDataReader reader = cmd.ExecuteReader();
                        i = 0;
                        lock(resources) {
                            resources.Clear();
                        }
                        while(reader.Read()) {
                            int resourceId = -1;
                            try {
                                resourceId = int.Parse(reader[0].ToString());
                                lock(resources) {
                                    resources.Add(resourceId, new PendingTranslation(logHandler, i, reader[1].ToString()));
                                }
                            } catch(Exception ex) {
                                logHandler(new StatusMessage(string.Format("Error parsing resource #{0}: {1}", resourceId, ex.Message), StatusMessage.StatusMessageType.Error));
                            }
                            i++;
                            if(worker.CancellationPending) {
                                throw new Exception("Translation aborted");
                            }
                        }

                        logHandler(new StatusMessage("Queueing translations...", StatusMessage.StatusMessageType.Status));

                        translationsDone = new ManualResetEvent[resources.Count];
                        foreach(KeyValuePair<int, PendingTranslation> resource in resources) {
                            if(ThreadPool.QueueUserWorkItem(new WaitCallback(DoTranslation), resource.Key)) {
                                translationsDone[resource.Value.WaitIndex] = new ManualResetEvent(false);
                            } else {
                                translationsDone[resource.Value.WaitIndex] = new ManualResetEvent(true);
                                logHandler(new StatusMessage(string.Format("Queueing failed for resource #{0}", resource.Key), StatusMessage.StatusMessageType.Error));
                            }
                        }
                        reader.Close();
                        if(resources.Count == 0) {
                            throw new Exception("No resources found to translate");
                        }

                        translationsDone = new ManualResetEvent[i]; // sometimes there are mismatches if something failed, and the initial count is only a visual guide anyway
                        for(int j = 0; j < i; j++) {
                            translationsDone[j] = new ManualResetEvent(false);
                        }
                    }
                    db.CloseConnection(); // translating could take a while and there's no point tying up a DB connection
                    WaitHandle.WaitAll(translationsDone);
                    logHandler(new StatusMessage("\nSource language translation finished, storing translations...", StatusMessage.StatusMessageType.Status));
                    if(worker.CancellationPending) {
                        throw new Exception("Translation aborted");
                    }

                    using(Database.Command cmd = db.CreateCommand()) {
                        int numCopied = 0;
                        db.OpenConnection();
                        string querySuccessMessage,
                               queryErrorMessage;
                        string sourceEnglishName,
                               destinationEnglishName;
                        try {
                            sourceEnglishName = CultureInfo.GetCultureInfo(sourceLanguage).EnglishName;
                        } catch {
                            sourceEnglishName = sourceLanguage;
                        }
                        try {
                            destinationEnglishName = CultureInfo.GetCultureInfo(destinationLanguage).EnglishName;
                        } catch {
                            destinationEnglishName = destinationLanguage;
                        }
                        if(overwriteExisting) {
                            switch(db.InstanceType) {
                                case Database.DatabaseType.MySQL:
                                    cmd.CommandText = string.Format("INSERT INTO {0}resources (resourceid, language, translation, translatorcomment) VALUES (@tID, @lang, @trans, @comment) ON DUPLICATE KEY UPDATE translation = @trans, translatorcomment = @comment", db.TablePrefix);
                                    break;
                                case Database.DatabaseType.SQLServer:
                                    cmd.CommandText = string.Format("UPDATE {0}resources SET translation = @trans, translatorcomment = @comment WHERE resourceid = @tID AND language = @lang; IF @@ROWCOUNT = 0 INSERT INTO {0}resources (resourceid, language, translation, translatorcomment) VALUES (@tID, @lang, @trans, @comment)", db.TablePrefix);
                                    break;
                                case Database.DatabaseType.SQLite:
                                    cmd.CommandText = string.Format("INSERT OR REPLACE INTO {0}resources (resourceid, language, translation, translatorcomment) VALUES (@tID, @lang, @trans, @comment)", db.TablePrefix);
                                    break;
                            }
                            querySuccessMessage = "Overwriting translation for resource #{0}...";
                            queryErrorMessage = "Error adding translation for resource #{0}: {1}";
                        } else {
                            cmd.CommandText = string.Format("INSERT INTO {0}resources (resourceid, language, translation, translatorcomment) VALUES (@tID, @lang, @trans, @comment)", db.TablePrefix);
                            querySuccessMessage = "Adding translation for resource #{0}...";
                            queryErrorMessage = "Translation for resource #{0} exists, skipping..."; // errors thrown when a translation exists
                        }
                        cmd.AddParameterWithValue("@lang", destinationLanguage);
                        cmd.AddParameterWithValue("@comment", "Automatically translated using Google");
                        cmd.AddParameterWithValue("@tID", string.Empty);
                        cmd.AddParameterWithValue("@trans", string.Empty);

                        foreach(KeyValuePair<int, PendingTranslation> resource in resources) {
                            cmd.Parameters["@tID"].Value = resource.Key;
                            GoogleTranslator.TranslationResponse response = resource.Value.Translation;
                            if(response.Status == 200) { // any errors should have been logged during translation
                                try {
                                    cmd.Parameters["@trans"].Value = response.Data.TranslatedText;
                                    cmd.ExecuteNonQuery();
                                    logHandler(new StatusMessage(string.Format(querySuccessMessage, resource.Key), StatusMessage.StatusMessageType.Success));
                                    numCopied++;
                                } catch(Exception ex) {
                                    logHandler(new StatusMessage(string.Format(queryErrorMessage, resource.Key, ex.Message), overwriteExisting ? StatusMessage.StatusMessageType.Success : StatusMessage.StatusMessageType.Error));
                                }
                            }
                            if(worker.CancellationPending) {
                                throw new Exception("Translation aborted");
                            }
                        }
                        logHandler(new StatusMessage(string.Format("\nTranslated {0} resources from {1} ({2}) to {3} ({4})", numCopied, sourceEnglishName, GoogleTranslator.GoogleSupportedLanguages[sourceType], destinationEnglishName, GoogleTranslator.GoogleSupportedLanguages[destinationType]), StatusMessage.StatusMessageType.Status));                        
                    }
                } catch(Exception ex) {
                    dwe.Result = ex.Message;
                    logHandler(new StatusMessage(ex.Message, StatusMessage.StatusMessageType.Error));
                } finally {
                    if(db != null) {
                        try {
                            db.CloseConnection();
                        } catch { }
                    }
                    ThreadPool.SetMinThreads(minWorkerThreads, maxIOThreads);
                    ThreadPool.SetMaxThreads(maxWorkerThreads, maxIOThreads);
                }
            };
            worker.RunWorkerCompleted += (s, rwe) => {
                if(rwe.Result != null && rwe.Result is string) {
                    DialogBox.ShowAlert(this, rwe.Result.ToString(), "Error");
                } else {
                    this.success = true;
                }
                prgStatus.Visibility = Visibility.Hidden;
                btnAbort.Visibility = Visibility.Collapsed;
                btnClose.Visibility = Visibility.Visible;
                txtProgress.Focus();
            };
            txtProgress.Inlines.Clear();
            prgStatus.Visibility = Visibility.Visible;
            btnClose.Visibility = Visibility.Collapsed;
            btnAbort.Visibility = Visibility.Visible;
            worker.RunWorkerAsync();
        }

        /// <summary>
        /// Translates a PendingTranslation.
        /// </summary>
        /// <param name="obj">The PendingTranslation.</param>
        private static void DoTranslation(object obj) {
            int resourceId = (int)obj;
            PendingTranslation translation;
            lock(resources) {
                translation = resources[resourceId];
            }
            GoogleTranslator.TranslationResponse response = GoogleTranslator.Translate(translation.NativeText, sourceType, destinationType);
            translation.Translation = response;
            if(response.Status == 200) {
                if(verbose) {
                    translation.LogHandler(new StatusMessage(string.Format("Translated resource #{0}", resourceId), StatusMessage.StatusMessageType.Success));
                    translation.LogHandler(new StatusMessage(string.Format("\t{0} => {1}", Utils.TrimText(translation.NativeText, 30), Utils.TrimText(response.Data.TranslatedText, 30)), StatusMessage.StatusMessageType.Other));
                } else {
                    translation.LogHandler(new StatusMessage(string.Format("Translated resource #{0}", resourceId), StatusMessage.StatusMessageType.Success));
                }
            } else {
                translation.LogHandler(new StatusMessage(string.Format("Error translating resource #{0}: {1} - {2}", resourceId, response.Status, response.Details), StatusMessage.StatusMessageType.Error));
            }
            translationsDone[translation.WaitIndex].Set();
        }

        /// <summary>
        /// Determines whether the background thread can be aborted
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.CanExecuteRoutedEventArgs"/> instance containing the event data.</param>
        private void CanExecuteAbort(object sender, CanExecuteRoutedEventArgs e) {
            e.CanExecute = worker != null && worker.IsBusy && !worker.CancellationPending;
        }

        /// <summary>
        /// Aborts the BackgroundWorker.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        private void Abort(object sender, RoutedEventArgs e) {
            if(worker != null && worker.IsBusy && worker.WorkerSupportsCancellation) {
                try {
                    worker.CancelAsync();
                } catch { }
            }
        }

        /// <summary>
        /// Appends text to the log textbox.
        /// </summary>
        /// <param name="text">The text.</param>
        /// <param name="error"><c>true</c> if the text is an error.</param>
        private void LogText(string text, StatusMessage.StatusMessageType type) {
            Run run = new Run(text + "\n");
            switch(type) {
                case StatusMessage.StatusMessageType.Error:
                    run.Style = styError;
                    break;
                case StatusMessage.StatusMessageType.Success:
                    run.Style = stySuccess;
                    break;
                case StatusMessage.StatusMessageType.Status:
                    run.Style = styStatus;
                    break;
                case StatusMessage.StatusMessageType.Other:
                    run.Style = styOther;
                    break;
            }
            txtProgress.Inlines.Add(new Span(run));
        }

        /// <summary>
        /// Closes the window.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
        private void CloseWindow(object sender, ExecutedRoutedEventArgs e) {
            Close();
        }

        /// <summary>
        /// A machine translation queued for execution by the threadpool.
        /// </summary>
        private class PendingTranslation {
            /// <summary>
            /// Gets or sets the log delegate.
            /// </summary>
            /// <value>The delegate used to log messages.</value>
            public StatusMessage.StatusChangedEventHandler LogHandler { get; private set; }

            /// <summary>
            /// Gets or sets the index of this translation in the array of reset events.
            /// </summary>
            /// <value>The index of the wait.</value>
            public int WaitIndex { get; private set; }

            /// <summary>
            /// Gets or sets the native text (in the source language).
            /// </summary>
            /// <value>The native text (in the source language).</value>
            public string NativeText { get; private set;  }

            /// <summary>
            /// Gets or sets the translation received from Google.
            /// </summary>
            /// <value>The translation.</value>
            public GoogleTranslator.TranslationResponse Translation { get; set; }

            /// <summary>
            /// Initializes a new <see cref="PendingTranslation"/>.
            /// </summary>
            /// <param name="logHandler">The delegate used to log messages.</param>
            /// <param name="waitIndex">The index of this translation in the array of reset events..</param>
            /// <param name="nativeText">The native text (in the source language).</param>
            public PendingTranslation(StatusMessage.StatusChangedEventHandler logHandler, int waitIndex, string nativeText) {
                this.LogHandler = logHandler;
                this.WaitIndex = waitIndex;
                this.NativeText = nativeText;
            }
        }
    }
}
